﻿using System;
using System.Collections.Generic;
using System.Text;
using System.Windows;
using System.Runtime.InteropServices;
using System.Diagnostics;

namespace AOC19NET {
	/// <summary>
	/// Auxiliar class that will read the Winamp's process memory to obtain the current playing file.
	/// </summary>
	class WinampReader {
		/*-------------------------------------------------------------------------*/
		/* IMPORTS																   */
		/*-------------------------------------------------------------------------*/
		#region DLL Imports
			[DllImport("user32.dll", CharSet=CharSet.Auto)]
			public static extern IntPtr FindWindow([MarshalAs(UnmanagedType.LPTStr)] string lpClassName,
												   [MarshalAs(UnmanagedType.LPTStr)] string lpWindowName);
			[DllImport("user32.dll", CharSet=CharSet.Auto)]
			public static extern int SendMessageA(IntPtr hwnd, int wMsg, int wParam, uint lParam);

			[DllImport("user32.dll", CharSet=CharSet.Auto)]
			public static extern int GetWindowText(IntPtr hwnd, string lpString, int cch);

			[DllImport("user32.dll")]
			private static extern int GetWindowThreadProcessId(IntPtr hWnd, out int ProcessId);

			[DllImport("kernel32.dll", SetLastError=true)]
			static extern bool ReadProcessMemory(IntPtr hProcess, IntPtr lpBaseAddress, [Out()] byte[] lpBuffer, int dwSize, out int lpNumberOfBytesRead);
		#endregion

		/*-------------------------------------------------------------------------*/
		/* DEFINITIONS															   */
		/*-------------------------------------------------------------------------*/
		#region DEFINITIONS
			// To tell Winamp that we are sending it a WM_COMMAND it needs the hex code 0x111
			const int WM_COMMAND = 0x111;

			// To tell Winamp that we are sending it a WM_USER (WM_WA_IPC) it needs the hex code 0x0400
			const int WM_WA_IPC = 0x0400;

			// Returns the playlist position
			const int IPC_GETLISTPOS = 125;

			// Returns the full path for the given playlist position
			const int IPC_GETPLAYLISTFILE = 211;

			// Returns the time info for the current song
			const int IPC_GETOUTPUTTIME = 105;
		#endregion

		/*-------------------------------------------------------------------------*/
		/* MEMBERS																   */
		/*-------------------------------------------------------------------------*/
		#region MEMBERS
		/// <summary>
		/// Handler to the Winamp Window.
		/// </summary>
		private IntPtr m_WinampWnd;

		/// <summary>
		/// Full path to the song currently playing.
		/// </summary>
		private String m_sCurrentFilename;

		/// <summary>
		/// Index in the Winamp's playlist of the song currently playing.
		/// We'll use this info to check if a new song has arrived.
		/// </summary>
		private int m_iCurrentPlaylistIndex;

		/// <summary>
		/// Total duration of the song currently playing.
		/// We'll use this info to check if a new song has arrived.
		/// </summary>
		private TimeSpan m_CurrentDuration;
		#endregion

		/*-------------------------------------------------------------------------*/
		/* METHODS																   */
		/*-------------------------------------------------------------------------*/
		#region METHODS
		/// <summary>
		/// Constructor of the class: sets all the members to its default values.
		/// </summary>
		public WinampReader() {
			m_WinampWnd = IntPtr.Zero;
			m_sCurrentFilename = "";
			m_iCurrentPlaylistIndex = -1;
			m_CurrentDuration = new TimeSpan(0, 0, 0);
		}

		/// <summary>
		/// Tries to find the Winamp window handler.
		/// </summary>
		/// <returns>True if the Winamp window was found and was valid.</returns>
		public bool FindWinamp() {
			m_WinampWnd = FindWindow("Winamp v1.x", null);
			return (m_WinampWnd != IntPtr.Zero);
		}

		/// <summary>
		/// Obtains the full path of the current track. Since reading the path from the Winamp's process is quite
		/// expensive, we keep track of the index within the playlist and read the path only when it changes.
		/// </summary>
		/// <returns>The full path of the current track.</returns>
		public String GetPlayingTrackPath() {
			// Check if the Winamp window has been found
			if(m_WinampWnd == IntPtr.Zero) {
				return "ERROR";
			}

			// 1. Retrieve the index within the playlist of the current track
			int iTrackPos = SendMessageA(m_WinampWnd, WM_WA_IPC, 0, IPC_GETLISTPOS);

			// Only read the filename if the track has changed, otherwise return the stored name.
			// This can cause a bug when a new playlist is loaded and the active song have the same index than the active song of the old playlist.
			// To avoid that, we'll additionally check if the song length has also changed, minimizing the risk a lot.
			// It could still happen to change to a new playlist where the active song has the same index and length, but it's much more unlikely!
			TimeSpan NewDuration = GetTotalTime();
			if(iTrackPos != m_iCurrentPlaylistIndex || NewDuration != m_CurrentDuration) {
				// 2. Retrieve complete filename from Winamp by reading directly from memory
				// 2.1. Get the memory position where the path is stored
				IntPtr pRemoteBuffer = (IntPtr)SendMessageA(m_WinampWnd, WM_WA_IPC, iTrackPos, IPC_GETPLAYLISTFILE);

				// 2.2. Open a handler to the Winamp's process
				int iWinampProcessId = 0;
				GetWindowThreadProcessId(m_WinampWnd, out iWinampProcessId);
				if(iWinampProcessId == 0) {
					return "ERROR";
				}
				IntPtr pWinampProcessHandler = Process.GetProcessById(iWinampProcessId).Handle;

				// 2.3. Do the reading and obtain the String
				if(pWinampProcessHandler != IntPtr.Zero) {
					// Read from process memory
					byte[] vData = new byte[256];
					int iRead;
					ReadProcessMemory(pWinampProcessHandler, pRemoteBuffer, vData, 256, out iRead);

					// Convert to char[] and clean the string (remove zero characters and replace '\' by '/')
					int i = 0;
					char[] vCharData = new char[iRead];
					while(vData[i] != 0 && i < 256) {
						if(vData[i] == (byte)'\\') {
							vData[i] = (byte)'/';
						}

						vCharData[i] = (char)vData[i];
						i++;
					}

					// Convert to String and store new values
					m_sCurrentFilename = new String(vCharData, 0, i);
					m_iCurrentPlaylistIndex = iTrackPos;
					m_CurrentDuration = NewDuration;
				}
			}
			
			//Console.WriteLine(sPath);
			return m_sCurrentFilename;
		}

		/// <summary>
		/// Sends a message to the Winamp window to obtain the elapsed time for the current song and return it in the form of TimeSpan.
		/// </summary>
		/// <returns>Elapsed time of the current song.</returns>
		public TimeSpan GetElapsedTime() {
			TimeSpan ElapsedTime = new TimeSpan(0, 0, 0);
			
			// Check if the Winamp window has been found
			if(m_WinampWnd == IntPtr.Zero) {
				return ElapsedTime;
			}

			// Get elapsed time
			long lElapsedTime = SendMessageA(m_WinampWnd, WM_WA_IPC, 0, IPC_GETOUTPUTTIME);	// 0 for elapsed ms
			if(lElapsedTime < 0) {
				lElapsedTime = 0;
			}
			ElapsedTime = new TimeSpan(lElapsedTime * 10000);
			return ElapsedTime;
		}

		/// <summary>
		/// Sends a message to the Winamp window to obtain the duration of the current song and return it in the form of TimeSpan.
		/// </summary>
		/// <returns>Total time of the current song.</returns>
		public TimeSpan GetTotalTime() {
			TimeSpan TotalTime = new TimeSpan(0, 0, 0);

			// Check if the Winamp window has been found
			if(m_WinampWnd == IntPtr.Zero) {
				return TotalTime;
			}

			// Get duration
			long lTotalTime = SendMessageA(m_WinampWnd, WM_WA_IPC, 2, IPC_GETOUTPUTTIME);	// 2 for total ms
			if(lTotalTime < 0) {
				lTotalTime = 0;
			}
			TotalTime = new TimeSpan(lTotalTime * 10000);
			return TotalTime;
		}
		#endregion
	}
}
